{"values":{"name":"Prekės klasės pridėjimas egzistuojančioms prekėms","code":"JM17856","description":"Prekės klasė pridedama pagal papildomą lauką 'Prekės Kokybės Kategorija'","triggerTypeId":0,"placeId":"multiple-place","scriptContent":"// Product Class Update from ext.pNote1\nconst context = createProcessingContext();\nconst MAX_CONCURRENCY = 5;\nconst TIME_CHECK_INTERVAL = 20;\n\nconst date = new Date();\nconst formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n\ntry {\n  log.info(\"Pradedamas produktų gavimas...\");\n  const allProducts = await getProducts();\n  log.info(`Gauta ${allProducts.length} produktų`);\n\n  // Filter products that have ext.pNote1\n  const filteredProducts = allProducts.filter(product => {\n    const ext = product.exts?.find(e => e.id?.name === 'ext.pNote1');\n    return ext && ext.value;\n  });\n\n  log.info(filteredProducts);\n\n  if (filteredProducts.length === 0) {\n    log.info(\"Nerasta produktų su ext.pNote1\");\n    output = {\n      executionStatus: \"SUCCESS\",\n      message: \"Nerasta produktų su ext.pNote1 atnaujinimui\",\n      processed: 0,\n      successful: 0,\n      failed: 0,\n      skipped: 0\n    };\n    return;\n  }\n\n  context.statistics.totalCount = filteredProducts.length;\n  log.info(`Rasta ${filteredProducts.length} produktų su ext.pNote1. Pradedamas apdorojimas...`);\n\n  // Process products in parallel\n  const results = await processProductsInParallel(filteredProducts, context, MAX_CONCURRENCY);\n\n  context.statistics.successful = results.filter(r => r.status === 'success').length;\n  context.statistics.failed = results.filter(r => r.status === 'error').length;\n  context.statistics.skipped = results.filter(r => r.status === 'skipped').length;\n  context.reportData.results = results;\n\n  log.info(`Apdorojimas baigtas: ${context.statistics.successful} sėkmingų, ${context.statistics.failed} klaidų, ${context.statistics.skipped} praleista`);\n\n  let link = '';\n\n  // Generate report only if there are failed items\n  const failedResults = results.filter(r => r.status === 'error');\n  \n  if (failedResults.length > 0) {\n    try {\n      const excelReport = await generateExcelReport(\n        failedResults,\n        context.statistics,\n        formattedDate,\n        context.executionState.timedOut\n      );\n\n      const company = await fetchCompany(companyId);\n      \n      if (company?.client?.id) {\n        const fileUploadResponse = await fileService.uploadFile(\n          excelReport,\n          `product-class-update-report-${formattedDate}.xlsx`,\n          {\n            fileType: \"ATTACHMENT\",\n            association: {\n              companyId: companyId,\n              clientId: company.client.id,\n              clientCode: company.client.code,\n              clientName: company.client.name,\n            },\n            isTemporary: true,\n            generatePublicLink: true,\n          }\n        );\n        link = ` <a href=\"${fileUploadResponse.publicLink}\" target=\"_blank\" style=\"color: red;\">Peržiūrėti klaidų ataskaitą</a>`;\n      } else {\n        log.error(\"Nepavyko gauti įmonės duomenų\");\n      }\n    } catch (error) {\n      log.error(`Nepavyko sugeneruoti ataskaitos: ${error.message}`);\n    }\n  }\n\n  const { executionStatus, message } = determineExecutionStatus(context);\n\n  output = {\n    executionStatus,\n    message: message + link,\n    processed: context.statistics.totalCount,\n    successful: context.statistics.successful,\n    failed: context.statistics.failed,\n    skipped: context.statistics.skipped,\n    timedOut: context.executionState.timedOut\n  };\n\n} catch (error) {\n  log.error(`Kritinė klaida: ${error.message}`);\n  output = { \n    executionStatus: \"FAILED\",\n    message: `Kritinė klaida: ${error.message}` \n  };\n}\n\n// ============================================================================\n// Processing functions\n// ============================================================================\n\nasync function processProductsInParallel(products, context, maxConcurrency) {\n  const results = new Array(products.length).fill(null);\n  let nextIndex = 0;\n  let completedCount = 0;\n  let stopProcessing = false;\n\n  const remainingTime = await getRemainingSessionTime();\n  if (remainingTime < 315) {\n    log.error(`Nepakanka laiko apdorojimui (${remainingTime}s)`);\n    context.executionState.timedOut = true;\n    return [];\n  }\n\n  const activePromises = new Map();\n\n  function addTask() {\n    if (nextIndex < products.length && !stopProcessing) {\n      const index = nextIndex++;\n      const product = products[index];\n      \n      const promise = processProduct(product)\n        .then(result => {\n          activePromises.delete(promise);\n          return { index, result };\n        })\n        .catch(error => {\n          activePromises.delete(promise);\n          const errorMsg = error.title || error.message || JSON.stringify(error);\n          log.error(`Klaida apdorojant produktą ${product.id}: ${errorMsg}`);\n          return { \n            index, \n            result: { \n              productId: product.id,\n              productCode: product.code || '',\n              newValue: getExtValue(product),\n              status: 'error', \n              reason: errorMsg \n            }\n          };\n        });\n      \n      activePromises.set(promise, index);\n      return promise;\n    }\n    return null;\n  }\n\n  // Fill initial pool\n  for (let i = 0; i < Math.min(maxConcurrency, products.length); i++) {\n    addTask();\n  }\n\n  while (activePromises.size > 0) {\n    const completedTask = await Promise.race(activePromises.keys());\n    results[completedTask.index] = completedTask.result;\n    completedCount++;\n\n    // Log only errors\n    if (completedTask.result.status === 'error') {\n      log.error(`[${completedCount}/${products.length}] Nepavyko: ${completedTask.result.productId} - ${completedTask.result.reason}`);\n    }\n\n    // Progress log every 100 items\n    if (completedCount % 100 === 0) {\n      log.info(`Apdorota: ${completedCount}/${products.length}`);\n    }\n\n    // Time check\n    if (completedCount % TIME_CHECK_INTERVAL === 0 && !stopProcessing) {\n      const remainingSeconds = await getRemainingSessionTime();\n      if (remainingSeconds < 315) {\n        log.error(`Liko mažiau nei 315s (${remainingSeconds}s). Stabdomas apdorojimas.`);\n        stopProcessing = true;\n        context.executionState.timedOut = true;\n      }\n    }\n\n    if (!stopProcessing) {\n      addTask();\n    }\n  }\n\n  return results.filter(r => r !== null);\n}\n\nasync function processProduct(product) {\n  const extValue = getExtValue(product);\n  const productId = product.id;\n  const productCode = product.code || '';\n\n  if (!extValue) {\n    return { \n      productId, \n      productCode,\n      newValue: '', \n      status: 'skipped', \n      reason: 'Tuščia ext.pNote1 reikšmė' \n    };\n  }\n\n  try {\n    await mutate('updatePartialProduct', {\n      productId: productId,\n      fields: { productClass: extValue }\n    });\n\n    return { \n      productId, \n      productCode,\n      newValue: extValue, \n      status: 'success', \n      reason: 'Sėkmingai atnaujinta' \n    };\n  } catch (error) {\n    const errorMsg = error.title || error.message || JSON.stringify(error);\n    // Log error immediately\n    log.error(`Nepavyko atnaujinti produkto ${productId}: ${errorMsg}`);\n    return { \n      productId, \n      productCode,\n      newValue: extValue, \n      status: 'error', \n      reason: errorMsg \n    };\n  }\n}\n\nfunction getExtValue(product) {\n  const ext = product.exts?.find(e => e.id?.name === 'ext.pNote1');\n  return ext?.value || '';\n}\n\n// ============================================================================\n// Data fetching\n// ============================================================================\n\nasync function getProducts() {\n  const filters = {\n    \"type\": \"SERVICE\",\n  };\n  \n  const response = await fetchExportData(\"PRODUCT\", {\n    filters,\n    dataSelection: [\n      \"/products/id\",\n      \"/products/code\",\n      \"/products/exts\"\n    ]\n  });\n  \n  return response.success?.data?.products || [];\n}\n\nasync function fetchCompany(companyId) {\n  try {\n    const result = await rql(`\n      @ALL SELECT client.name, client.address, client.id, client.code\n      FROM companies\n      WHERE id = ${companyId}\n    `);\n    return result?.[0] ?? null;\n  } catch (error) {\n    log.error(`Klaida gaunant įmonės duomenis: ${error.message}`);\n    return null;\n  }\n}\n\n// ============================================================================\n// Status & Context\n// ============================================================================\n\nfunction createProcessingContext() {\n  return {\n    statistics: {\n      totalCount: 0,\n      successful: 0,\n      skipped: 0,\n      failed: 0\n    },\n    executionState: {\n      timedOut: false\n    },\n    reportData: {\n      results: []\n    }\n  };\n}\n\nfunction determineExecutionStatus(context) {\n  const { statistics, executionState } = context;\n  const { totalCount, successful, skipped, failed } = statistics;\n\n  if (executionState.timedOut) {\n    return {\n      executionStatus: \"WARNING\",\n      message: `Apdorojimas sustabdytas dėl laiko limito. Atnaujinta ${successful}, praleista ${skipped}, nepavyko ${failed}.`\n    };\n  }\n\n  if (totalCount === 0) {\n    return {\n      executionStatus: \"SUCCESS\",\n      message: \"Nerasta duomenų apdorojimui.\"\n    };\n  }\n\n  if (successful === 0 && skipped === 0 && failed > 0) {\n    return {\n      executionStatus: \"FAILED\",\n      message: `Nepavyko atnaujinti jokių produktų. Klaidų: ${failed}.`\n    };\n  }\n\n  if (failed > 0) {\n    return {\n      executionStatus: \"WARNING\",\n      message: `Atnaujinta ${successful}, praleista ${skipped}, nepavyko ${failed}.`\n    };\n  }\n\n  return {\n    executionStatus: \"SUCCESS\",\n    message: `Sėkmingai atnaujinta ${successful} produktų, praleista ${skipped}.`\n  };\n}\n\n// ============================================================================\n// Excel Report (only failed items)\n// ============================================================================\n\nasync function generateExcelReport(failedResults, statistics, date, timedOut) {\n  const xlsx = new XLSXBuilder();\n  const sheet = xlsx.addSheet('Klaidos');\n\n  sheet.columnWidth('A', 40);\n  sheet.columnWidth('B', 20);\n  sheet.columnWidth('C', 20);\n  sheet.columnWidth('D', 50);\n\n  // Title\n  sheet.merge('A1:D1');\n  sheet.setCursor('A1');\n  sheet.setStyle({ font: { bold: true, size: 14 } });\n  sheet.append('Produktų klasės atnaujinimo klaidos');\n  sheet.resetStyle();\n\n  sheet.merge('A2:D2');\n  sheet.setCursor('A2');\n  sheet.append(date);\n\n  // Summary\n  const summaryData = [\n    ['Iš viso apdorota:', statistics.totalCount],\n    ['Sėkmingai:', statistics.successful],\n    ['Praleista:', statistics.skipped],\n    ['Nepavyko:', statistics.failed]\n  ];\n\n  if (timedOut) {\n    summaryData.push(['Būsena:', 'Sustabdyta dėl laiko limito']);\n  }\n\n  summaryData.forEach((row, index) => {\n    const rowNum = 4 + index;\n    sheet.setCursor(`A${rowNum}`);\n    sheet.setStyle({ font: { bold: true } });\n    sheet.append(row[0]);\n    sheet.resetStyle();\n    sheet.setCursor(`B${rowNum}`);\n    sheet.append(row[1]);\n  });\n\n  // Header row for failed items\n  const headerRowNum = 4 + summaryData.length + 1;\n\n  sheet.setCursor(`A${headerRowNum}`);\n  sheet.setStyle({ font: { bold: true } });\n  sheet.append([['Produkto ID', 'Produkto kodas', 'Nauja reikšmė', 'Klaidos pranešimas']]);\n  sheet.resetStyle();\n\n  const headerStyle = {\n    font: { bold: true },\n    fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'E0E0E0' } },\n    border: {\n      top: { style: 'thin', color: { argb: '000000' } },\n      left: { style: 'thin', color: { argb: '000000' } },\n      bottom: { style: 'thin', color: { argb: '000000' } },\n      right: { style: 'thin', color: { argb: '000000' } }\n    }\n  };\n  sheet.cellStyle(`A${headerRowNum}:D${headerRowNum}`, headerStyle);\n\n  // Data rows (only failed)\n  let currentRow = headerRowNum + 1;\n  \n  const borderStyle = {\n    border: {\n      top: { style: 'thin', color: { argb: '000000' } },\n      left: { style: 'thin', color: { argb: '000000' } },\n      bottom: { style: 'thin', color: { argb: '000000' } },\n      right: { style: 'thin', color: { argb: '000000' } }\n    }\n  };\n\n  const errorStyle = {\n    fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFB3BA' } },\n    ...borderStyle\n  };\n\n  failedResults.forEach(result => {\n    sheet.setCursor(`A${currentRow}`);\n    sheet.append([[\n      result.productId,\n      result.productCode,\n      result.newValue,\n      result.reason\n    ]]);\n    sheet.cellStyle(`A${currentRow}:D${currentRow}`, errorStyle);\n    currentRow++;\n  });\n\n  return await xlsx.toBuffer();\n}","paramsFormEnabled":false,"paramsFormSchema":"/**\nČia galite aprašyti parametrų formos schemą\n\n[\n\t{\n\t\t\"blockType\": BlockTypeEnum.SELECT,\n\t\t\"meta\": {\n\t\t\t\"fieldName\": \"client\",\n\t\t\t\"title\": \"Klientas\",\n\t\t\t\"size\": 12,\n\t\t\t\"meta\": {\n\t\t\t\t\"getItems\": \"rql:@MDPAGE SELECT * FROM clients\"\n\t\t\t}\n\t\t}\n\t}\n]\n*/","exampleData":"{\n\t\"id\": \"ba8b1704-a27e-4de3-acde-92b4407da4d1\"\n}","activeFrom":null,"activeTo":null,"active":true,"appDefinitionId":"ed57460c-6450-45b2-805f-4a9a072aef61"},"additionalPlaces":[]}